#ifndef ZSERIO_PMR_POLYMORPHIC_ALLOCATOR_H_INC
#define ZSERIO_PMR_POLYMORPHIC_ALLOCATOR_H_INC

#include <limits>
#include <type_traits>
#include <utility>

#include "zserio/Types.h"
#include "zserio/pmr/MemoryResource.h"

namespace zserio
{
namespace pmr
{
namespace detail
{

/**
 * Polymorphic allocator inspired by C++17 standard.
 */
template <class T = uint8_t>
class PolymorphicAllocatorBase
{
public:
    using value_type = T;

    // Following typedefs are only present for compatibility with older std. libraries, that does not fully
    // conform to C++11. [old-compiler-support]
    using pointer = value_type*;
    using const_pointer = const value_type*;
    using size_type = size_t;
    using difference_type = ptrdiff_t;
    using reference = value_type&;
    using const_reference = const value_type&;

    /**
     * Constructor.
     *
     * Note that this is intentionally non-explicit to allow to pass MemoryResource wherever
     * the PolymorphicAllocator is required.
     *
     * \param resource Memory resource. According to the C++ standard the resource may not be NULL. Since it
     *                 implies undefined behaviour, we define a non-standard extension here. When the resource
     *                 is NULL, getDefaultResource() is used instead!
     */
    PolymorphicAllocatorBase(MemoryResource* resource = getDefaultResource()) noexcept :
            m_resource(resource != nullptr ? resource : getDefaultResource()) // non-standard extension
    {}

    /**
     * Method generated by default.
     * \{
     */
    ~PolymorphicAllocatorBase() = default;

    PolymorphicAllocatorBase(const PolymorphicAllocatorBase& other) noexcept = default;
    PolymorphicAllocatorBase& operator=(const PolymorphicAllocatorBase& other) noexcept = default;

    PolymorphicAllocatorBase(PolymorphicAllocatorBase&& other) noexcept = default;
    PolymorphicAllocatorBase& operator=(PolymorphicAllocatorBase&& other) noexcept = default;
    /**
     * \}
     */

    /**
     * Copy constructor from PolymorphicAllocator with another value_type.
     *
     * \param other Other PolymorphicAllocator.
     */
    template <class U>
    PolymorphicAllocatorBase(const PolymorphicAllocatorBase<U>& other) noexcept :
            m_resource(other.resource())
    {}

    /**
     * Assignment operator from PolymorphicAllocator with another value_type.
     *
     * \param other Other PolymorphicAllocator.
     */
    template <class U>
    PolymorphicAllocatorBase& operator=(const PolymorphicAllocatorBase<U>& other) noexcept
    {
        m_resource = other.resource();
        return *this;
    }

    /**
     * Allocates memory for n values.
     *
     * \param size Number of values to allocate memory for.
     */
    value_type* allocate(size_t size)
    {
        return static_cast<value_type*>(m_resource->allocate(size * sizeof(value_type), alignof(value_type)));
    }

    /**
     * Deallocates memory for n values.
     *
     * \param memory Pointer to the memory to deallocate.
     * \param size Number of values held by the memory pointed to by memory.
     *         Shall be the same size as was used for allocation of memory.
     */
    void deallocate(value_type* memory, size_t size) noexcept
    {
        m_resource->deallocate(memory, size * sizeof(value_type), alignof(value_type));
    }

    /**
     * Gets the underlying memory resource.
     */
    MemoryResource* resource() const noexcept
    {
        return m_resource;
    }

    /**
     * Constructs an object in allocated memory.
     *
     * \param ptr Pointer to memory where the object will be constructed.
     * \param args Parameters to be forwarded to the object constructor.
     * \note This is only necessary for compatibility with older std. libraries, that does not fully
     * conform to C++11. [old-compiler-support]
     */
    template <typename U, typename... Args>
    void construct(U* ptr, Args&&... args) noexcept(
            noexcept(new (static_cast<void*>(ptr)) U(std::forward<Args>(args)...)))
    {
        new (static_cast<void*>(ptr)) U(std::forward<Args>(args)...);
    }

    /**
     * Destroys an object
     *
     * \param ptr Pointer to the object to be destroyed.
     * \note This is only necessary for compatibility with older std. libraries, that does not fully
     * conform to C++11. [old-compiler-support]
     */
    template <typename U>
    void destroy(U* ptr) noexcept(noexcept(ptr->~U()))
    {
        ptr->~U();
    }

    /**
     * Returns theoretical maximal limit for allocation.
     *
     * \return Theoretical maximal limit for allocation.
     * \note This is only necessary for compatibility with older std. libraries, that does not fully
     * conform to C++11. [old-compiler-support]
     */
    size_type max_size() const noexcept
    {
        return std::numeric_limits<size_type>::max() / sizeof(value_type);
    }

private:
    MemoryResource* m_resource;
};

template <class T, class U>
bool operator==(const PolymorphicAllocatorBase<T>& lhs, const PolymorphicAllocatorBase<U>& rhs) noexcept
{
    return *lhs.resource() == *rhs.resource();
}

template <class T, class U>
bool operator!=(const PolymorphicAllocatorBase<T>& lhs, const PolymorphicAllocatorBase<U>& rhs) noexcept
{
    return !(lhs == rhs);
}

} // namespace detail

/**
 * Non-propagating version of the polymorphic allocator. This allocator behaves as the polymorphic_allocator
 * from C++17.
 */
template <class T = uint8_t>
class PolymorphicAllocator : public detail::PolymorphicAllocatorBase<T>
{
public:
    using detail::PolymorphicAllocatorBase<T>::PolymorphicAllocatorBase;

    using propagate_on_container_copy_assignment = std::false_type;
    using propagate_on_container_move_assignment = std::false_type;
    using propagate_on_container_swap = std::false_type;

    /**
     * Returns instance of the allocator to be used when a container gets copied.
     */
    PolymorphicAllocator select_on_container_copy_construction() const
    {
        return PolymorphicAllocator();
    }

    /**
     * Rebind template.
     * \note This is only necessary for compatibility with older std. libraries, that does not fully
     * conform to C++11. [old-compiler-support]
     */
    template <typename U>
    struct rebind
    {
        using other = PolymorphicAllocator<U>;
    };
};

/**
 * Propagating version of the polymorphic allocator. This one is propagated on container copy and assignment.
 */
template <class T = uint8_t>
class PropagatingPolymorphicAllocator : public detail::PolymorphicAllocatorBase<T>
{
public:
    using detail::PolymorphicAllocatorBase<T>::PolymorphicAllocatorBase;

    using propagate_on_container_copy_assignment = std::true_type;
    using propagate_on_container_move_assignment = std::true_type;
    using propagate_on_container_swap = std::true_type;

    /**
     * Returns instance of the allocator to be used when a container gets copied.
     */
    PropagatingPolymorphicAllocator select_on_container_copy_construction() const
    {
        return *this;
    }

    /**
     * Rebind template.
     * \note This is only necessary for compatibility with older std. libraries, that does not fully
     * conform to C++11. [old-compiler-support]
     */
    template <typename U>
    struct rebind
    {
        using other = PropagatingPolymorphicAllocator<U>;
    };
};

} // namespace pmr
} // namespace zserio

#endif // ZSERIO_PMR_POLYMORPHIC_ALLOCATOR_H_INC
